#include "QtFlowScene.hpp"

#include <stdexcept>
#include <utility>

#include <QtWidgets/QGraphicsSceneMoveEvent>
#include <QtWidgets/QFileDialog>
#include <QtCore/QByteArray>
#include <QtCore/QBuffer>
#include <QtCore/QDataStream>
#include <QtCore/QFile>

#include <QtCore/QJsonDocument>
#include <QtCore/QJsonObject>
#include <QtCore/QJsonArray>
#include <QtCore/QtGlobal>
#include <QtCore/QDebug>

#include "QtNode.hpp"
#include "QtNodeGraphicsObject.hpp"

#include "QtNodeGraphicsObject.hpp"
#include "QtConnectionGraphicsObject.hpp"

#include "QtConnection.hpp"

#include "QtFlowView.hpp"
#include "QtDataModelRegistry.hpp"

namespace Qt
{
	QtFlowScene::
		QtFlowScene(std::shared_ptr<QtDataModelRegistry> registry,
			QObject * parent)
		: QGraphicsScene(parent)
		, _registry(std::move(registry))
	{
		setItemIndexMethod(QGraphicsScene::NoIndex);

		// This connection should come first
		connect(this, &QtFlowScene::connectionCreated, this, &QtFlowScene::setupConnectionSignals);
		connect(this, &QtFlowScene::connectionCreated, this, &QtFlowScene::sendConnectionCreatedToNodes);
		connect(this, &QtFlowScene::connectionDeleted, this, &QtFlowScene::sendConnectionDeletedToNodes);
	}

	QtFlowScene::
		QtFlowScene(QObject * parent)
		: QtFlowScene(std::make_shared<QtDataModelRegistry>(),
			parent)
	{}


	QtFlowScene::
		~QtFlowScene()
	{
		clearScene();
	}


	//------------------------------------------------------------------------------

	std::shared_ptr<QtConnection>
		QtFlowScene::
		createConnection(PortType connectedPort,
			QtNode& node,
			PortIndex portIndex)
	{
		auto connection = std::make_shared<QtConnection>(connectedPort, node, portIndex);

		auto cgo = detail::make_unique<QtConnectionGraphicsObject>(*this, *connection);

		// after this function connection points are set to node port
		connection->setGraphicsObject(std::move(cgo));

		_connections[connection->id()] = connection;

		// Note: this connection isn't truly created yet. It's only partially created.
		// Thus, don't send the connectionCreated(...) signal.

		connect(connection.get(),
			&QtConnection::connectionCompleted,
			this,
			[this](QtConnection const& c) {
				connectionCreated(c);
			});

		return connection;
	}


	std::shared_ptr<QtConnection>
		QtFlowScene::
		createConnection(QtNode& nodeIn,
			PortIndex portIndexIn,
			QtNode& nodeOut,
			PortIndex portIndexOut,
			TypeConverter const &converter)
	{
		auto connection =
			std::make_shared<QtConnection>(nodeIn,
				portIndexIn,
				nodeOut,
				portIndexOut,
				converter);

		auto cgo = detail::make_unique<QtConnectionGraphicsObject>(*this, *connection);

		nodeIn.nodeState().setConnection(PortType::In, portIndexIn, *connection);
		nodeOut.nodeState().setConnection(PortType::Out, portIndexOut, *connection);

		// after this function connection points are set to node port
		connection->setGraphicsObject(std::move(cgo));

		// trigger data propagation
		nodeOut.onDataUpdated(portIndexOut);

		_connections[connection->id()] = connection;

		connectionCreated(*connection);

		return connection;
	}


	std::shared_ptr<QtConnection>
	QtFlowScene::restoreConnection(QJsonObject const &connectionJson)
	{
		QUuid nodeInId = QUuid(connectionJson["in_id"].toString());
		QUuid nodeOutId = QUuid(connectionJson["out_id"].toString());

		PortIndex portIndexIn = connectionJson["in_index"].toInt();
		PortIndex portIndexOut = connectionJson["out_index"].toInt();

		auto nodeIn = _nodes[nodeInId].get();
		auto nodeOut = _nodes[nodeOutId].get();

		auto getConverter = [&]()
		{
			QJsonValue converterVal = connectionJson["converter"];

			if (!converterVal.isUndefined())
			{
				QJsonObject converterJson = converterVal.toObject();

				NodeDataType inType{ converterJson["in"].toObject()["id"].toString(),
									  converterJson["in"].toObject()["name"].toString() };

				NodeDataType outType{ converterJson["out"].toObject()["id"].toString(),
									   converterJson["out"].toObject()["name"].toString() };

				auto converter =
					registry().getTypeConverter(outType, inType);

				if (converter)
					return converter;
			}

			return TypeConverter{};
		};

		std::shared_ptr<QtConnection> connection =
			createConnection(*nodeIn, portIndexIn,
				*nodeOut, portIndexOut,
				getConverter());

		// Note: the connectionCreated(...) signal has already been sent
		// by createConnection(...)

		return connection;
	}

	void QtFlowScene::deleteConnection(QtConnection& connection)
	{
		auto it = _connections.find(connection.id());
		if (it != _connections.end())
		{
			connection.removeFromNodes();
			_connections.erase(it);
		}
	}


	QtNode&
		QtFlowScene::
		createNode(std::unique_ptr<QtNodeDataModel> && dataModel)
	{
		auto node = detail::make_unique<QtNode>(std::move(dataModel));
		auto ngo = detail::make_unique<QtNodeGraphicsObject>(*this, *node);

		node->setGraphicsObject(std::move(ngo));

		auto nodePtr = node.get();
		_nodes[node->id()] = std::move(node);

		nodeCreated(*nodePtr);
		return *nodePtr;
	}


	QtNode&
		QtFlowScene::
		restoreNode(QJsonObject const& nodeJson)
	{
		QString modelName = nodeJson["model"].toObject()["name"].toString();

		auto dataModel = registry().create(modelName);

		if (!dataModel)
			throw std::logic_error(std::string("No registered model with name ") +
				modelName.toLocal8Bit().data());

		auto node = detail::make_unique<QtNode>(std::move(dataModel));
		auto ngo = detail::make_unique<QtNodeGraphicsObject>(*this, *node);
		node->setGraphicsObject(std::move(ngo));

		node->restore(nodeJson);

		auto nodePtr = node.get();
		_nodes[node->id()] = std::move(node);

		nodePlaced(*nodePtr);
		nodeCreated(*nodePtr);
		return *nodePtr;
	}


	void
		QtFlowScene::
		removeNode(QtNode& node)
	{
		// call signal
		nodeDeleted(node);

		clearNode(node);
	}

	void QtFlowScene::clearNode(QtNode& node)
	{
		for (auto portType : { PortType::In,PortType::Out })
		{
			auto nodeState = node.nodeState();
			auto const& nodeEntries = nodeState.getEntries(portType);

			for (auto& connections : nodeEntries)
			{
				for (auto const& pair : connections)
					deleteConnection(*pair.second);
			}
		}

		_nodes.erase(node.id());
	}

	QtDataModelRegistry&
		QtFlowScene::
		registry() const
	{
		return *_registry;
	}


	void
		QtFlowScene::
		setRegistry(std::shared_ptr<QtDataModelRegistry> registry)
	{
		_registry = std::move(registry);
	}


	void
		QtFlowScene::
		iterateOverNodes(std::function<void(QtNode*)> const & visitor)
	{
		for (const auto& _node : _nodes)
		{
			visitor(_node.second.get());
		}
	}


	void
		QtFlowScene::
		iterateOverNodeData(std::function<void(QtNodeDataModel*)> const & visitor)
	{
		for (const auto& _node : _nodes)
		{
			visitor(_node.second->nodeDataModel());
		}
	}


	void
		QtFlowScene::
		iterateOverNodeDataDependentOrder(std::function<void(QtNodeDataModel*)> const & visitor)
	{
		std::set<QUuid> visitedNodesSet;

		//A leaf node is a node with no input ports, or all possible input ports empty
		auto isNodeLeaf =
			[](QtNode const &node, QtNodeDataModel const &model)
		{
			for (unsigned int i = 0; i < model.nPorts(PortType::In); ++i)
			{
				auto connections = node.nodeState().connections(PortType::In, i);
				if (!connections.empty())
				{
					return false;
				}
			}

			return true;
		};

		//Iterate over "leaf" nodes
		for (auto const &_node : _nodes)
		{
			auto const &node = _node.second;
			auto model = node->nodeDataModel();

			if (isNodeLeaf(*node, *model))
			{
				visitor(model);
				visitedNodesSet.insert(node->id());
			}
		}

		auto areNodeInputsVisitedBefore =
			[&](QtNode const &node, QtNodeDataModel const &model)
		{
			for (size_t i = 0; i < model.nPorts(PortType::In); ++i)
			{
				auto connections = node.nodeState().connections(PortType::In, i);

				for (auto& conn : connections)
				{
					if (visitedNodesSet.find(conn.second->getNode(PortType::Out)->id()) == visitedNodesSet.end())
					{
						return false;
					}
				}
			}

			return true;
		};

		//Iterate over dependent nodes
		while (_nodes.size() != visitedNodesSet.size())
		{
			for (auto const &_node : _nodes)
			{
				auto const &node = _node.second;
				if (visitedNodesSet.find(node->id()) != visitedNodesSet.end())
					continue;

				auto model = node->nodeDataModel();

				if (areNodeInputsVisitedBefore(*node, *model))
				{
					visitor(model);
					visitedNodesSet.insert(node->id());
				}
			}
		}
	}


	QPointF
		QtFlowScene::
		getNodePosition(const QtNode& node) const
	{
		return node.nodeGraphicsObject().pos();
	}


	void
		QtFlowScene::
		setNodePosition(QtNode& node, const QPointF& pos) const
	{
		node.nodeGraphicsObject().setPos(pos);
		node.nodeGraphicsObject().moveConnections();
	}


	QSizeF
		QtFlowScene::
		getNodeSize(const QtNode& node) const
	{
		return QSizeF(node.nodeGeometry().width(), node.nodeGeometry().height());
	}


	std::unordered_map<QUuid, std::unique_ptr<QtNode> > const &
		QtFlowScene::
		nodes() const
	{
		return _nodes;
	}


	std::unordered_map<QUuid, std::shared_ptr<QtConnection> > const &
		QtFlowScene::
		connections() const
	{
		return _connections;
	}


	std::vector<QtNode*>
		QtFlowScene::
		allNodes() const
	{
		std::vector<QtNode*> nodes;

		std::transform(_nodes.begin(),
			_nodes.end(),
			std::back_inserter(nodes),
			[](std::pair<QUuid const, std::unique_ptr<QtNode>> const & p) { return p.second.get(); });

		return nodes;
	}


	std::vector<QtNode*>
		QtFlowScene::
		selectedNodes() const
	{
		QList<QGraphicsItem*> graphicsItems = selectedItems();

		std::vector<QtNode*> ret;
		ret.reserve(graphicsItems.size());

		for (QGraphicsItem* item : graphicsItems)
		{
			auto ngo = qgraphicsitem_cast<QtNodeGraphicsObject*>(item);

			if (ngo != nullptr)
			{
				ret.push_back(&ngo->node());
			}
		}

		return ret;
	}


	//------------------------------------------------------------------------------

	void
		QtFlowScene::
		clearScene()
	{
		//Manual node cleanup. Simply clearing the holding datastructures doesn't work, the code crashes when
		// there are both nodes and connections in the scene. (The data propagation internal logic tries to propagate
		// data through already freed connections.)
		while (_connections.size() > 0)
		{
			deleteConnection(*_connections.begin()->second);
		}

		while (_nodes.size() > 0)
		{
			clearNode(*_nodes.begin()->second);
		}
	}


	void
		QtFlowScene::
		save() const
	{
		QString fileName =
			QFileDialog::getSaveFileName(nullptr,
				tr("Open Flow Scene"),
				QDir::homePath(),
				tr("Flow Scene Files (*.flow)"));

		if (!fileName.isEmpty())
		{
			if (!fileName.endsWith("flow", Qt::CaseInsensitive))
				fileName += ".flow";

			QFile file(fileName);
			if (file.open(QIODevice::WriteOnly))
			{
				file.write(saveToMemory());
			}
		}
	}


	void
		QtFlowScene::
		load()
	{
		QString fileName =
			QFileDialog::getOpenFileName(nullptr,
				tr("Open Flow Scene"),
				QDir::homePath(),
				tr("Flow Scene Files (*.flow)"));

		if (!QFileInfo::exists(fileName))
			return;

		QFile file(fileName);

		if (!file.open(QIODevice::ReadOnly))
			return;

		clearScene();

		QByteArray wholeFile = file.readAll();

		loadFromMemory(wholeFile);
	}


	QByteArray
		QtFlowScene::
		saveToMemory() const
	{
		QJsonObject sceneJson;

		QJsonArray nodesJsonArray;

		for (auto const & pair : _nodes)
		{
			auto const &node = pair.second;

			nodesJsonArray.append(node->save());
		}

		sceneJson["nodes"] = nodesJsonArray;

		QJsonArray connectionJsonArray;
		for (auto const & pair : _connections)
		{
			auto const &connection = pair.second;

			QJsonObject connectionJson = connection->save();

			if (!connectionJson.isEmpty())
				connectionJsonArray.append(connectionJson);
		}

		sceneJson["connections"] = connectionJsonArray;

		QJsonDocument document(sceneJson);

		return document.toJson();
	}


	void
		QtFlowScene::
		loadFromMemory(const QByteArray& data)
	{
		QJsonObject const jsonDocument = QJsonDocument::fromJson(data).object();

		QJsonArray nodesJsonArray = jsonDocument["nodes"].toArray();

		for (QJsonValueRef node : nodesJsonArray)
		{
			restoreNode(node.toObject());
		}

		QJsonArray connectionJsonArray = jsonDocument["connections"].toArray();

		for (QJsonValueRef connection : connectionJsonArray)
		{
			restoreConnection(connection.toObject());
		}
	}


	void
		QtFlowScene::
		setupConnectionSignals(QtConnection const& c)
	{
		connect(&c,
			&QtConnection::connectionMadeIncomplete,
			this,
			&QtFlowScene::connectionDeleted,
			Qt::UniqueConnection);
	}


	void
		QtFlowScene::
		sendConnectionCreatedToNodes(QtConnection const& c)
	{
		QtNode* from = c.getNode(PortType::Out);
		QtNode* to = c.getNode(PortType::In);

		Q_ASSERT(from != nullptr);
		Q_ASSERT(to != nullptr);

		from->nodeDataModel()->outputConnectionCreated(c);
		to->nodeDataModel()->inputConnectionCreated(c);

		this->nodeInputUpdated(*to);
	}


	void
		QtFlowScene::
		sendConnectionDeletedToNodes(QtConnection const& c)
	{
		QtNode* from = c.getNode(PortType::Out);
		QtNode* to = c.getNode(PortType::In);

		Q_ASSERT(from != nullptr);
		Q_ASSERT(to != nullptr);

		from->nodeDataModel()->outputConnectionDeleted(c);
		to->nodeDataModel()->inputConnectionDeleted(c);
	}


	//------------------------------------------------------------------------------
	QtNode*
		locateNodeAt(QPointF scenePoint, QtFlowScene &scene,
			QTransform const & viewTransform)
	{
		// items under cursor
		QList<QGraphicsItem*> items =
			scene.items(scenePoint,
				Qt::IntersectsItemShape,
				Qt::DescendingOrder,
				viewTransform);

		//// items convertable to NodeGraphicsObject
		std::vector<QGraphicsItem*> filteredItems;

		std::copy_if(items.begin(),
			items.end(),
			std::back_inserter(filteredItems),
			[](QGraphicsItem * item)
			{
				return (dynamic_cast<QtNodeGraphicsObject*>(item) != nullptr);
			});

		QtNode* resultNode = nullptr;

		if (!filteredItems.empty())
		{
			QGraphicsItem* graphicsItem = filteredItems.front();
			auto ngo = dynamic_cast<QtNodeGraphicsObject*>(graphicsItem);

			resultNode = &ngo->node();
		}

		return resultNode;
	}
}